Understanding Multi-Tenancy: What It Actually Means in Practice
1. Introduction: What is Multi-Tenancy, Really?
I’ve always heard people talk about multi-tenancy, but for the longest time, it felt like one of those buzzwords that companies throw around—like "enterprise-grade" or "scalable architecture"—without actually explaining what it means.
So, I finally sat down to figure it out. What exactly is multi-tenancy, and why does it matter?
At its core, multi-tenancy (usually) refers to a single application that serves multiple customers (tenants) while keeping their data isolated.
Does Multi-Tenancy Always Refer to the Data?**
No! Multi-tenancy can apply to:
- The database (one DB per tenant, schema per tenant, or row-level)
- The application (one Go app handling all tenants, or separate instances per tenant)
- The infrastructure (separate compute resources, like per-tenant EC2 instances, containers, or Kubernetes pods)
Since most people usually refer to data multi tenancy, we will start by covering that first.
Data Multi-Tenancy
The Three Main Types of Data Multi-Tenancy**
There are multiple ways to implement multi-tenancy, each with different trade-offs:
Multi-Tenancy Approach | How It Works | Pros | Cons |
---|---|---|---|
Database-per-Tenant (Separate Databases) | Each customer gets their own database within the same instance or across instances. | ✅ Strong isolation ✅ Easier data migration per tenant | ❌ Higher resource usage ❌ Harder to manage at scale |
Schema-per-Tenant (Shared DB, Separate Schemas) | One database, but each tenant has a dedicated schema (tenant1.users , tenant2.users ). | ✅ Better isolation than row-level ✅ Scales better than per-db | ❌ Harder to manage schema changes ❌ More complex migrations |
Row-Based Multi-Tenancy (Shared DB, Shared Schema) | All tenants’ data is in the same tables, with tenant_id as a key. | ✅ Most scalable ✅ Simplest to manage at scale | ❌ Harder to enforce isolation ❌ Risk of data leaks |
Each of these models has use cases where it makes the most sense.
A. Database-Per-Tenant: The Cleanest but Most Expensive Approach
This is the approach that seems most intuitive—each customer gets their own database.
How It Works
- A single Go application serves multiple tenants.
- When a customer logs in, the app dynamically selects the correct database connection:
func getDBForTenant(tenantID string) (*sql.DB, error) {
dsn := fmt.Sprintf("postgres://user:pass@rds-instance/%s", tenantID)
return sql.Open("postgres", dsn)
} - Each tenant has full isolation from others—no risk of cross-tenant data leaks.
Why Use This Approach?
✅ Security & Isolation – No accidental data mix-ups.
✅ Easier per-tenant scaling – High-usage customers can be moved to their own RDS instance.
✅ Simple database operations – Backups, restores, and migrations can be done per customer.
Why It’s Not Always Ideal
❌ Higher cost – Each database takes up resources, even for small customers.
❌ Connection limits – A database-per-tenant model can hit DB connection pool limits.
❌ Harder to manage at scale – Hundreds or thousands of databases become painful to maintain.
Best for:
- High-security applications (e.g., healthcare, finance).
- Large customers who need isolated data environments.
- When strict data residency (e.g., per-country data laws) is required.
B. Schema-Per-Tenant: The Middle Ground
Instead of separate databases, each tenant has their own schema within the same database:
tenant1.users
tenant2.orders
tenant3.logs
How It Works in Go
When querying, you dynamically set the schema for each request:
func setSchemaForTenant(db *sql.DB, tenantID string) error {
_, err := db.Exec(fmt.Sprintf("SET search_path TO tenant_%s", tenantID))
return err
}
Why This Approach?
✅ More scalable than per-database – One DB, many schemas.
✅ Still provides isolation – Tenants don’t share tables.
✅ Easier maintenance than per-database – Fewer connections, single backup strategy.
Challenges
❌ Schema management is harder – Every migration has to update all schemas.
❌ Less isolation than per-database – A misconfigured query could still affect another tenant.
Best for:
- SaaS applications with many small tenants.
- Medium security requirements where full database isolation isn’t needed.
- When scaling beyond hundreds of tenants but still wanting some isolation.
C. Row-Based Multi-Tenancy: The Most Scalable but Risky Approach
Instead of separate databases or schemas, all tenants’ data is stored in shared tables. Each table includes a tenant_id
column:
SELECT * FROM users WHERE tenant_id = 123;
How It Works in Go
Every query must include tenant filtering:
func getUsersForTenant(db *sql.DB, tenantID int) ([]User, error) {
query := "SELECT * FROM users WHERE tenant_id = $1"
rows, err := db.Query(query, tenantID)
return scanUsers(rows), err
}
Why This Approach?
✅ Best performance & scalability – No need to manage thousands of schemas or databases.
✅ Simplifies onboarding – No need to provision a new DB/schema per customer.
✅ Efficient resource usage – Fewer connections, easier caching.
Challenges
❌ Biggest risk of data leaks – One incorrect query (SELECT * FROM users
without WHERE tenant_id = X
) could expose all customer data.
❌ More complex permissioning – Every API call must be scoped properly.
❌ Harder to migrate tenants – Moving a single customer to their own DB is non-trivial.
Best for:
- High-scale SaaS products with thousands of customers.
- Low-security use cases where cross-tenant data leaks aren’t catastrophic.
- Products where all tenants follow the same schema (no per-customer customizations).
Choosing the Right Data Multi-Tenancy Model**
Scenario | Best Multi-Tenancy Approach |
---|---|
High-security, compliance-heavy app | ✅ Database-per-tenant |
Mid-size SaaS with many customers | ✅ Schema-per-tenant |
Large-scale SaaS with thousands of users | ✅ Row-based multi-tenancy |
Small startup, unsure about scale | ✅ Start with schema-per-tenant or per-db, migrate later |
7. Conclusion
- Multi-tenancy isn’t one-size-fits-all—it’s a spectrum.
- Database-per-tenant is the cleanest but expensive.
- Schema-per-tenant balances cost & isolation for SaaS apps.
- Row-based multi-tenancy is the most scalable but comes with risks.
If you’re building a new SaaS or multi-tenant system, your best bet is to start simple and migrate later if needed.
Multi-Tenancy at the Application Level
While most discussions on multi-tenancy focus on data (database, schema, or row-level isolation), you can also implement multi-tenancy at the application level by spinning up separate Go applications for each tenant.
This is less common but could make sense in specific scenarios.
1. Single Application, Multi-Tenant (Most Common)
- One Go application serves all tenants.
- Multi-tenancy is handled at the data level (separate DBs, schemas, or row-level filtering).
- Requests are dynamically routed to the right tenant’s data.
- Example: SaaS platform where all customers use
app.example.com
.
✅ Simple to maintain
✅ Efficient use of compute resources
❌ More complex data access control
❌ Scaling individual tenants is harder
2. Multi-App, One per Tenant (Less Common)
- Each customer (tenant) gets their own Go application instance running separately.
- This could be a dedicated EC2, Docker container, or Kubernetes pod per tenant.
- Requests are routed to the appropriate application instance.
- Example: High-security SaaS platforms where each customer gets a dedicated deployment.
✅ Strong isolation between customers
✅ Can customize app behavior per tenant
✅ Easier to scale heavy-use customers independently
❌ Expensive—each tenant needs its own compute resources
❌ Harder to deploy updates across all tenants
You might see this in:
- Enterprise SaaS with custom code per customer
- Government & healthcare applications requiring strict data separation
- High-value customers who demand dedicated infrastructure
3. Hybrid: Multi-App, but Shared Compute
- Similar to one app per tenant, but instead of spinning up a full EC2 per tenant, you run multiple tenants' apps inside the same compute environment.
- Could be multi-container (Docker, ECS, Kubernetes) where each container handles a different tenant.
- Uses a reverse proxy to route requests to the right tenant’s instance.
✅ More efficient than fully separate instances
✅ Each tenant can have custom logic while sharing compute
✅ Still allows scaling individual tenants separately
❌ More DevOps complexity
❌ Still needs careful resource management
Example:
- A SaaS provider with a "premium" plan where big customers get their own isolated instance, but small customers use the shared multi-tenant version.
Which Approach is Best?
Scenario | Best Multi-Tenant Model |
---|---|
Small SaaS, shared compute | ✅ Single app, multi-tenant database |
High-security enterprise SaaS | ✅ Dedicated app per tenant |
SaaS with custom features per tenant | ✅ Hybrid approach: per-tenant app but shared infrastructure |
Cost-sensitive SaaS scaling globally | ✅ Single app, multi-tenant DB with schema-per-tenant |
But in most discussions, people refer to database multi-tenancy because it’s the most common and cost-effective approach. Spinning up a separate Go app per tenant is usually reserved for high-security or enterprise SaaS models.